package dungeonElements;

import java.util.HashMap;

import be.kuleuven.cs.som.annotate.*;
import dungeon.RoomHandler;
import dungeonTools.Direction;

/**
 * A class of borders. Each border describes a border between two squares.
 * A square is linked to a border in a direction. This direction is the OPPOSITE
 * of the direction the square has this border in.
 * 
 * e.g. : A square has this border to its EAST
 * -> This border has that square to its WEST
 * 
 * @invar Each border must have one or two proper bordering squares.
 * 		| hasProperBorderingSquares()
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 */
public abstract class Border {
	
	/**
	 * Initialize this new border with the given square as bordering square 
	 * in the given direction.
	 * 
	 * @param square
	 * 		The bordering square of this border.
	 * @param direction
	 * 		The direction of bordering square.
	 * @effect The square is set as bordering square in the given direction.
	 * 		| setSquareInDirection(square, direction)
	 */
	@Raw 
	public Border(Square square, Direction direction) {
		setSquareInDirection(square, direction);
	}
	
	
	/**
	 * @return True if this border is a obstacle. 
	 */
	public abstract boolean isObstacle();
			
	/**
	 * Check whether this border is terminated.
	 */
	@Basic @Raw
	public boolean isTerminated() {
		return this.isTerminated;
	}
	
	/**
	 * Terminate this border.
	 * 
	 * @post This border is terminated.
	 * 		| new.isTerminated()
	 * @post If this border was not already terminated, this border no longer references 
	 * 		squares as bordering squares.
	 * 		| for each direction in Direction:
	 * 		| 	if( !isTerminated() )
	 * 		|		then new.hasSquareInDirection(direction) == false
	 * @post If this border was not already terminated, the bordering squares 
	 * 		no longer have this border as there border.
	 * 		| for each direction in Direction:
	 * 		|	if( !isTerminated() )
	 * 		|		then (!(new getSquareInDirection(direction))
	 * 		|				.hasBorderInDirection(direction.getOppositeDirection()))
	 */
	@Raw
	public void terminate(){
		if(!isTerminated()){
			this.isTerminated = true;
			for(Direction allDirections : Direction.values() ){
				if(this.hasSquareInDirection(allDirections)){
					Square formerSquare = this.getSquareInDirection(allDirections);
					removeSquareInDirection(allDirections); 
					if( formerSquare.getBorder(allDirections.getOppositeDirection())==this )
						formerSquare.setBorder(null, allDirections.getOppositeDirection());
				}
			}
		}
	}

	/**
	 * Variable registering whether this border is terminated.
	 */
	private boolean isTerminated = false;
	
	
	/**
	 * Return the bordering square in the given direction. 
	 * 
	 * @param direction
	 * 		The direction of the bordering square to return.
	 * @throws IllegalArgumentException
	 * 		The direction must be effective
	 * 		| direction == null
	 * @throws IllegalArgumentException
	 * 		The border must have a square in the given direction.
	 * 		| !hasSquareInDirection(direction)
	 */
	@Basic @Raw
	public Square getSquareInDirection(Direction direction) throws IllegalArgumentException{
		if(direction == null)
			throw new IllegalArgumentException("Direction not initialized");
		if(!hasSquareInDirection(direction))
			throw new IllegalArgumentException("No square found at that direction");
		return borderingSquares.get(direction);
	}
	
	/**
	 * Check whether this border has a square in the given direction.
	 * 
	 * @param direction
	 * 		The direction to check.
	 * @return True if and only if this border has a square in the given direction.
	 * 		| if(getSquareInDirection(Direction direction)!=null)
	 * 		|	then result == true
	 * 		| else result == false
	 * @throws IllegalArgumentException
	 * 		direction must be initialized.
	 * 		| direction == null
	 */
	@Raw
	public boolean hasSquareInDirection(Direction direction) throws IllegalArgumentException{
		if(direction == null)
			throw new IllegalArgumentException("Direction not initialized");
		if(borderingSquares.containsKey(direction))
			return true;
		return false;
	}
	
	/**
	 * Check whether this border can have the given square in the given direction 
	 * as its bordering square.
	 * 
	 * @param square
	 * 		The square to check.
	 * @param direction
	 * 		The direction to check.
	 * @return If this border is terminated, the border can not have a bordering square
	 * 		| if(isTerminated())
	 * 		| 	then result == false
	 * 		Otherwise, true if and only the given square and direction are effective
	 * 		and the given square is not yet terminated and the given square 
	 * 		and direction do not conflict with other bordering squares.
	 * 		| if( ((square == null) || (direction == null)) )
	 * 		| 	then result == false
	 *		| else if(square.isTerminated()) 
	 *		|	then result == false
	 *		| else if( !hasSquareInDirection(direction)
	 *		|			&&!hasSquareInDirection(direction.getOppositeDirection()) )
	 *		|	then for each directionInDirection in Direction:
	 *		|		if( hasSquareInDirection(directionInDirection) )
	 *		|			then result == false;
	 *		| else
	 *		|	then result == true
	 */
	@Raw
	public boolean canHaveAsSquareInDirection(@Raw Square square, Direction direction){
		if (isTerminated())
			return false;
		if( (square == null) || (direction == null) )
			return false;
		if(square.isTerminated())
			return false;
		if( !borderingSquares.isEmpty() && !hasSquareInDirection(direction) 
				&& !hasSquareInDirection(direction.getOppositeDirection()) )
			return false;
		return true;
	}
	
	/**
	 * Check whether this border has proper bordering squares.
	 * 
	 * @return False if this border is terminated and has bordering squares.
	 * 		| if( isTerminated() )
	 * 		|	for each direction in Direction:
	 * 		|		if( hasSquareInDirection(direction) )
	 * 		|			then result == false
	 * @return False if the border is not terminated and has no borderings squares 
	 * 		or more than two bordering squares
	 * 		| if( !isTerminated() )
	 * 		|	for each direction in Direction:
	 * 		|		if( hasSquareInDirection(direction) )
	 * 		|			countSquares++
	 * 		| 	if( countSquares==0 || countSquares>2 )
	 * 		|		then result == false
	 * @return	False if the border is not terminated and the border has a bordering square
	 *		in a direction that is not the opposite direction of an other bordering square.
	 * 		| if( !isTerminated() )
	 * 		|	for each direction of a bordering square in this border:
	 * 		|		if( hasSquareInConflictiongDirection(direction)) )
	 * 		|			then result == false
	 * @return False if the border is not terminated and the border has bordering squares
	 * 		witch do not have this border as border.
	 * 		| if( !isTerminated() )
	 * 		|	for each direction of a bordering square in this border:
	 * 		|		if( getSquareInDirection(direction).getBorder(direction.getOppositeDirection())!=this )
	 * 		|			then result == false
	 */
	@Raw
	public boolean hasProperBorderingSquares(){
		if( borderingSquares.isEmpty() || (borderingSquares.size() >2) ){
			if( borderingSquares.isEmpty() && isTerminated() )
				return true;
			else
				return false;
		}
		for(Direction direction : Direction.values() ){
			if(hasSquareInDirection(direction)){
				if( !canHaveAsSquareInDirection(getSquareInDirection(direction),direction) )
					return false;
				// Check for conflicting directions
				if( hasSquareInConflictingDirection(direction) )
					return false;
				// Check if square references back to border 
				if ( !(getSquareInDirection(direction).getBorder(direction.getOppositeDirection()) == this) ) 
					return false;
			}
		}
		return true;
	}
	
	/**
	 * Check whether this border has squares in a conflicting directions 
	 * with the given direction.
	 * 
	 * @param direction
	 * 		The Direction to check.
	 * @return True if and only if this square has squares in conflicting directions
	 * 		with the given direction.
	 * 		| for each directionInDirection in Direction:
	 * 		|	if(hasSquareInDirection(directionInDirection) 
	 *		|		&& ((directionInDirection!=direction.getOppositeDirection())
	 *		|				||(directionInDirection!=direction)) )
	 * @throws IllegalArgumentException
	 * 		The direction must be effective
	 * 		| direction == null
	 */
	@Raw
	public boolean hasSquareInConflictingDirection(Direction direction)
		throws IllegalArgumentException
	{
		if( direction == null )
			throw new IllegalArgumentException("Direction not initialized");
		for(Direction allDirections : Direction.values() ){
			if( hasSquareInDirection(allDirections) 
					&& ((allDirections!=direction.getOppositeDirection())
							||(allDirections!=direction)) )
				return true;
			}
		return false;
	}
	
	/**
	 * Set the square to which this border is attached to the given square 
	 * in a given direction.
	 * 
	 * @param square
	 * 		The square to which this border must be attached.
	 * @param direction
	 * 		The direction in which the given square must be attached.
	 * @post This border has the given square as its bordering square in the given direction.
	 * 		| new.getSquareInDirection(direction) == square
	 * @post The given square reference this border as its border in 
	 * 		the given direction.
	 * 		| square.getBorder(direction.getOppositeDirection())==this
	 * @throws IllegalArgumentException
	 * 		Setting the given bordering square must result in a valid border.
	 * 		| !canHaveAsSquareInDirection(square, direction)
	 * @throws IllegalArgumentException
	 * 		Setting a bordering square in a direction in witch the border has already 
	 * 		a bordering square is impossible.
	 * 		| hasSquareInDirection(direction)
	 */
	@Raw  
	public void setSquareInDirection(Square square, Direction direction)
		throws IllegalArgumentException
	{
		if(!canHaveAsSquareInDirection(square,direction))
			throw new IllegalArgumentException("This border can not have the given square in the given direction");
		if(hasSquareInDirection(direction) && getSquareInDirection(direction).equals(square))
			return;
		if(hasSquareInDirection(direction))	
			throw new IllegalArgumentException("Has alredy a bordering square in that direction");
		borderingSquares.put(direction, square);
		if(square.getBorder(direction.getOppositeDirection())!=this)
			square.setBorder(this, direction.getOppositeDirection());
		RoomHandler.setBorder(square,this,direction.getOppositeDirection());
	}

	/**
	 * Remove the bordering square in a given direction.
	 * 
	 * @param direction
	 * 		The direction in which the square must be removed.
	 * @post This border no longer reference the bordering square in a given direction 
	 * 		as its bordering square
	 * 		| new.hasSquareInDirection(direction) == false
	 * @throws IllegalArgumentException
	 * 		The direction must be effective.
	 * 		| direction == null
	 * @throws IllegalArgumentException
	 * 		It must be possible to remove the square in the given direction
	 * 		| !canRemoveSquareInDirection(direction)
	 */
	@Raw
	void removeSquareInDirection(Direction direction) 
		throws IllegalArgumentException, IllegalStateException
	{
		if(direction == null)
			throw new IllegalArgumentException("Direction not initialized");
		if(!canRemoveSquareInDirection(direction))
			throw new IllegalStateException("Can not remove square");
		borderingSquares.remove(direction);
	}
	
	/**
	 * Check whether or not the square can be removed in the given direction.
	 * 
	 * @param direction 
	 * 		The direction to check.
	 * @return True if this border is terminated
	 * 		| if( this.isTerminated() )
	 * 		|	then result == true
	 * @return True if this this border is not terminated and has two bordering squares
	 * 		and this border has a bordering square in the given direction and the bordering square in 
	 * 		the given direction does not has this border as its border.
	 * 		| if( !this.isTerminated())&&hasSquareInDirection(direction)&&(borderingSquares.size()==2 )
	 * 		|	for each directionInDirection in Direction:
	 * 		|		if(hasSquareInDirection(directionInDirection))
	 * 		|			countSquares++
	 * 		|	if( countSquares==2 &&(getSquareInDirection(direction)
	 * 		|						.getBorder(direction.getOppositeDirection())!=this) )
	 * 		|		then result == true
	 */
	@Raw
	public boolean canRemoveSquareInDirection(Direction direction){
		if(this.isTerminated())
				return true;
		if( (!this.isTerminated())&&(borderingSquares.size()==2)&&hasSquareInDirection(direction) ){
			return ( (getSquareInDirection(direction).getBorder(direction.getOppositeDirection()) != this) );
		}
		return false;
	}
	
	/**
	 * Variable referencing the squares to which this border is a border 
	 * in some direction.
	 */
	private final HashMap<Direction, Square> borderingSquares = new HashMap<Direction, Square>();

}
